#include "camera.h"
#include "input.h"

#include "states.h"
#include "console.h"

#include "../Epsilon/window.h"
#include "../Epsilon/EpsilonInput/keyboard.h"
#define eKeyboard Epsilon::Keyboard
extern eKeyboard keyboard;

#ifdef HAS_D3D
#include <d3d9.h>
#include <d3dx9.h>
#endif

//#ifndef WIN32
#if 1
/// HACK
enum {
  DIK_RSHIFT,
  DIK_LSHIFT,
  DIK_RCONTROL,
  DIK_LCONTROL,
  DIK_W,
  DIK_UP,
  DIK_S,
  DIK_DOWN,
  DIK_A,
  DIK_LEFT,
  DIK_D,
  DIK_RIGHT
};
#include "cli_parse/structs.h" /// D3DXDegreeToRadian ...
#endif

#include "../Epsilon/Epsilon3D/OpenGL.h"

#include "../Epsilon/timer.h"
#include "../Epsilon/Epsilon3D/rendercontext.h"

void
LookAt (e3dMatrix4x4& mat, e3dVector3 x, e3dVector3 y, e3dVector3 z)
{
/// Silly stuff required for OpenGL...
  x.x = -x.x;
  y.x = -y.x;
  z.x = -z.x;

  mat.LookAt (x, y, z);

/// More silly stuff...
  mat.m11 = -mat.m11;
  mat.m12 = -mat.m12;
  mat.m13 = -mat.m13;
}

e3dCamera::e3dCamera (void)
{
  m_vEye  = e3dVector3 (0.0f,  0.0f,  0.0f);
  m_vLook = e3dVector3 (0.0f,  1.0f,  0.0f);
  m_vUp   = e3dVector3 (0.0f,  0.0f,  1.0f);

  // Create a direction vector
  m_vDirection = m_vLook - m_vEye;
  m_vDirection.Normalize ();

//  m_vDelta = e3dVector3 (0.0f, 0.0f, 0.0f);

  m_fSpeed     = 0.5f;
  m_bDeferred  = false;
  m_bMouselook = false;

  x_rot = 0.0f;
  y_rot = 0.0f;

  states->input->InstallListener (this);
}

void
e3dCamera::Move (eInput* input)
{
  // Don't do anything if the console is active...
  if (states->gui_console->IsActive ())
    return;

  static eTimer timer;
  timer.tick ();

  e3dVector3 vDirection;

  bool fly  = false;
  bool fast = false;

  float fSpeed = (timer.FrameInterval () / m_fSpeed) * 25.0f;

  // Create the direction vector
  vDirection = fwd_level_vec ();

  if ( input->KeyPressed (DIK_RSHIFT) ||
       input->KeyPressed (DIK_LSHIFT) ||
       states->cl_fly )
    fly = true;

  if (input->KeyPressed (DIK_RCONTROL) ||
      input->KeyPressed (DIK_LCONTROL))
    fast = true;

  if (fly)
    vDirection = fwd_vec ();

#ifdef HAS_D3D
  // Reverse the fwd vector in OpenGL
  if (! e3dDirect3D::IsDevicePresent ())
#endif
    vDirection = -vDirection;

//  if (keyboard.GetKey (eKeyboard::TextKey (eKeyboard::TextKeys::W)))
  if (input->KeyPressed (DIK_W) || input->KeyPressed (DIK_UP))
  {
    if (fast) {
      m_bDeferred = true;
//      m_vDelta = vDirection * (fSpeed * 120);

      m_vEye  += vDirection * (fSpeed * 120);
      m_vLook += vDirection * (fSpeed * 120);
    } else {
      m_bDeferred = true;
//      m_vDelta = vDirection * (fSpeed * 20);

      m_vEye  += vDirection * (fSpeed * 20);
      m_vLook += vDirection * (fSpeed * 20);
    }
  }

//  if (keyboard.GetKey (eKeyboard::TextKey (eKeyboard::TextKeys::S)))
  if (input->KeyPressed (DIK_S) || input->KeyPressed (DIK_DOWN))
  {
    if (fast) {
//      m_vDelta = -vDirection * (fSpeed * 120);
      m_bDeferred = true;

      m_vEye  -= vDirection * (fSpeed * 120);
      m_vLook -= vDirection * (fSpeed * 120);
    } else {
//      m_vDelta = -vDirection * (fSpeed * 20);
      m_bDeferred = true;

      m_vEye  -= vDirection * (fSpeed * 20);
      m_vLook -= vDirection * (fSpeed * 20);
    }
  }

//  if (keyboard.GetKey (eKeyboard::TextKey (eKeyboard::TextKeys::A)))
  if (input->KeyPressed (DIK_A) || input->KeyPressed (DIK_LEFT))
  {
    vDirection = right_level_vec ();

    if (fast) {
      m_vEye  -= vDirection * (fSpeed * 120);
      m_vLook -= vDirection * (fSpeed * 120);
      m_bDeferred = true;
    } else {
      m_vEye  -= vDirection * (fSpeed * 20);
      m_vLook -= vDirection * (fSpeed * 20);
      m_bDeferred = true;
    }
  }

//  if (keyboard.GetKey (eKeyboard::TextKey (eKeyboard::TextKeys::D)))
  if (input->KeyPressed (DIK_D) || input->KeyPressed (DIK_RIGHT))
  {
    vDirection = right_level_vec ();

    if (fast) {
      m_vEye  += vDirection * (fSpeed * 120);
      m_vLook += vDirection * (fSpeed * 120);
      m_bDeferred = true;
    } else {
      m_vEye  += vDirection * (fSpeed * 20);
      m_vLook += vDirection * (fSpeed * 20);
      m_bDeferred = true;
    }
  }
}

void
e3dCamera::Rotate (eInput* input)
{
  /* Finish any rotations or movements before starting
       new transformations! */
  FlushPendingTransforms ();

  float rel_x = (float)input->GetDeltaX ();
  float rel_y = (float)input->GetDeltaY ();

  /* Movements should be relative to the amount of time that has
       passed since the last time this function was called... */
  static eTimer timer;
  timer.tick ();
  float fSpeed = (timer.FrameInterval () / m_fSpeed) * 50.0f;

  /* ^^^ NOTE: Tick the timer even when mouselook is not engaged,
                 this keeps mouslook silky smooth. */

  if (! m_bMouselook)
    return;

  eWindow*    win  = input->GetWindow ();
  eWindowRect rect = win->getRect     ();

  /* (x,y) represents the center of the window in client coordinates. */
  const int x = (rect.rcClient.right  - rect.rcClient.left) >> 1;
  const int y = (rect.rcClient.bottom - rect.rcClient.top)  >> 1;

  /* Before we actually move the cursor, generate two fake input events
       such that any event generated by the Window Manager when moving
         the cursor will not create an infinite loop of mouse move events. */
  win->getMouse ()->MoveCursor      ( x, y, false );
  win->getMouse ()->MoveCursorDelta ( 0, 0, false );

  /* ^^^ The third parameter false, indicates that NO installed listener
           will be notified of these two events... again, they are to eliminate
             unwanted WM behaviour when calling SetCursorPos (...).
             
           We want delta x and delta y to be 0 after calling
             win->SetCursorPos (...)
  */

  win->SetCursorPos (x, y);

  /* This actually includes sensitivity information, which is
       supposed to be part of the eTB_Input class... consider
         altering in the future. */
  rel_x *= fSpeed;
  rel_y *= fSpeed;

  /* 5/28/2011 - This logic is flawed, relative x / y movement of
                   0 does not mean do nothing - it means, zero out
                     the rotation vector. */

  // Do nothing if the mouse hasn't moved...
  if (rel_x == 0.0f && rel_y == 0.0f) {
    return;
  }

  e3dMatrix4x4 matRotTilt, matRotZ, matRot;

  m_vLook.x = 1.0f;
  m_vLook.y = 0.0f;
  m_vLook.z = 0.0f;

  m_vLook += m_vEye;

  m_vUp.x = 0.0f;
  m_vUp.y = 0.0f;
  m_vUp.z = 1.0f;

  // Create a direction vector
  m_vDirection = m_vLook - m_vEye;
  m_vDirection.Normalize ();

  float dir = 1.0f;

#ifdef HAS_D3D
  if (e3dDirect3D::IsDevicePresent ())
    dir = -1.0f;
#endif

  x_rot += (rel_x / -360.0f) * 60.0f;
  y_rot += (rel_y / -360.0f) * 60.0f;

  /* Allow a full 180 degrees of motion when flying,
       120 degrees otherwise. */
  const float max_tilt = states->cl_fly ? 90.0f : 60.0f;

  if (y_rot > max_tilt)
    y_rot = max_tilt;
  else if (y_rot < -max_tilt)
    y_rot = -max_tilt;

  matRotTilt.RotationY (-D3DXToRadian (y_rot));
  matRotZ.RotationZ    ( D3DXToRadian (x_rot));

  matRot       = matRotTilt * matRotZ;
  m_vDirection = matRot.transform (m_vDirection);
  m_vUp        = matRot.transform (m_vUp);
  m_vLook      = m_vDirection + m_vEye;

  /* Defer matrix calculation until next call to Update (...) */
  m_bDeferred = true;
}

#include "../Epsilon/Epsilon3D/rendercontext.h"

void
e3dCamera::Update (eInput* input)
{
  /* Finish any rotations or movements before starting
       new transformations! */
  FlushPendingTransforms ();

  Move (input);

#if 0
  //set view matrix
  if (e3dDirect3D::IsDevicePresent ()) {
    LPDIRECT3DDEVICE9 dev = e3dDirect3D::GetDev ();
    dev->SetTransform (D3DTS_VIEW, (D3DMATRIX *)(&m_matView.m11));
  } else {
    glMatrixMode  (GL_MODELVIEW);
    glLoadMatrixf (&m_matView.m11);
  }
#endif
} //Update


void
e3dCamera::SetPos (float x, float y, float z)
{
  float old_x = m_vEye.x;
  float old_y = m_vEye.y;
  float old_z = m_vEye.z;

  m_vEye.x = x;
  m_vEye.y = y;
  m_vEye.z = z;

  float diff_x = m_vEye.x - old_x;
  float diff_y = m_vEye.y - old_y;
  float diff_z = m_vEye.z - old_z;

  m_vLook.x += diff_x;
  m_vLook.y += diff_y;
  m_vLook.z += diff_z;

  /* Defer matrix calculation until next call to Update (...) */
  m_bDeferred = true;
}

e3dVector3
e3dCamera::GetPos (void)
{
  return m_vEye;
}

void
e3dCamera::SetRot (float angle)
{
  /* Finish any rotations or movements before starting
       new transformations! */
  FlushPendingTransforms ();

  //eTB_Printf ("Rot angle: %f\n", angle);

  x_rot = angle;
  y_rot = 0.0f;

  e3dVector3   vDirection;
  e3dMatrix4x4 matRotZ;

  m_vLook.x = 1.0f;
  m_vLook.y = 0.0f;
  m_vLook.z = 0.0f;

  m_vLook += m_vEye;

  m_vUp.x = 0.0f;
  m_vUp.y = 0.0f;
  m_vUp.z = 1.0f;

  // Create a direction vector
  vDirection = m_vLook - m_vEye;
  vDirection.Normalize ();

  float dir = 1.0f;

#ifdef HAS_D3D
  if (e3dDirect3D::IsDevicePresent ())
    dir = -1.0f;
#endif

  /// XXX: TODO :: Port Me - <D3DXToRadian>
  matRotZ.RotationZ (D3DXToRadian (angle) * dir); /// In cli_parse/structs.h

  vDirection = matRotZ.transform (vDirection);
  m_vUp      = matRotZ.transform (m_vUp);

  m_vLook = vDirection + m_vEye;

  /* Defer matrix calculation until next call to Update (...) */
  m_bDeferred = true;
}

void
e3dCamera::OnMouse (Epsilon::Mouse state, Epsilon::Mouse last)
{
  eInput*  input     = states->input;
  eWindow* win       = input->GetWindow ();
  bool     mouselook = m_bMouselook; /* Cache the original mouselook status. */

  if (input->IsMouseButtonDown (eInput::Mouse::Right))
    m_bMouselook = true;
  else if (   input->IsMouseButtonDown (eInput::Mouse::Left) &&
            ( input->KeyPressed        (DIK_RCONTROL) ||
              input->KeyPressed        (DIK_LCONTROL) ||
              input->KeyPressed        (68992) /* Temp Hack for Fn */) )
    m_bMouselook = true;
  else
    m_bMouselook = false;

  
  /* The status of mouselook has changed... */
  if (mouselook != m_bMouselook) {
    if (m_bMouselook) {
      /* Hide ALL cursors (WM and rendered) */
      input->ShowCursor (false);
      input->SetHotSpot (input->GetCursorX (), input->GetCursorY ());
    }
    else {
      /* Show the rendered cursor (do not change HW cursor status...) */
      input->ShowCursor   (true);
      input->SetCursorPos (input->GetHotSpotX (), input->GetHotSpotY ());
    }
  }

  /* Call Rotate (...) even if mouselook is not enabled,
       because it causes a timer to tick, which smooths out
         mouse motion.

       >> If not called regularly, the first few movements using
            mouselook will be extremely jerky!
  */
  Rotate (input);
}

void
e3dCamera::FlushPendingTransforms (void)
{
  if (m_bDeferred) {
    LookAt (m_matView, m_vEye, m_vLook, m_vUp);
    m_bDeferred = false;
  }
}
